/*-------------------------------------------------------------------------
 *
 * hashpage.c
 *	  Postgres哈希访问方法的哈希表页面管理代码
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/hash/hashpage.c
 *
 * NOTES
 *	  Postgres哈希页面看起来像普通的关系页面。高地址处的不透明数据包括有关页面的信息，包括
 *	  页面是否为溢出页面或实际的桶，桶编号以及同一桶中的前后页面的块编号。
 *
 *	  哈希关系的第一页，即页0，是特殊的——它存储描述哈希表的信息；它被称为“元页面”。第一页及以上存储实际数据。
 *
 *	  还有位图页面，这里不处理；
 *	  见hashovfl.c。
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/hash.h"
#include "access/hash_xlog.h"
#include "access/xloginsert.h"
#include "miscadmin.h"
#include "port/pg_bitutils.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
#include "storage/smgr.h"

static bool fc__hash_alloc_buckets(Relation fc_rel, BlockNumber fc_firstblock,
								uint32 fc_nblocks);
static void fc__hash_splitbucket(Relation fc_rel, Buffer fc_metabuf,
							  Bucket fc_obucket, Bucket fc_nbucket,
							  Buffer fc_obuf,
							  Buffer fc_nbuf,
							  HTAB *fc_htab,
							  uint32 fc_maxbucket,
							  uint32 fc_highmask, uint32 fc_lowmask);
static void fc_log_split_page(Relation fc_rel, Buffer fc_buf);


/*
 *	_hash_getbuf() -- 通过块编号获取用于读或写的缓冲区。
 *
 *		'access' 必须是 HASH_READ、HASH_WRITE 或 HASH_NOLOCK。
 *		'flags' 是允许的页面类型的按位或。
 *
 *		这只能用于获取预期已有效的页面。使用给定的标志应用 _hash_checkpage()。
 *
 *		当这个例程返回时，请求的缓冲区上会设置适当的锁，并且它的引用计数已递增
 *		（即，缓冲区是“已锁定并固定”）。
 *
 *		P_NEW 是不允许的，因为这个例程只能用于访问已知在文件系统 EOF 之前的页面。
 *		扩展索引应该使用 _hash_getnewbuf 完成。
 */
Buffer _hash_getbuf(Relation fc_rel, BlockNumber fc_blkno, int fc_access, int fc_flags)
{
	Buffer		fc_buf;

	if (fc_blkno == P_NEW)
		elog(ERROR, "hash AM does not use P_NEW");

	fc_buf = ReadBuffer(fc_rel, fc_blkno);

	if (fc_access != HASH_NOLOCK)
		LockBuffer(fc_buf, fc_access);

	/* 引用计数和锁类型是正确的 */

	_hash_checkpage(fc_rel, fc_buf, fc_flags);

	return fc_buf;
}

/*
 * _hash_getbuf_with_condlock_cleanup() -- 尝试获取用于清理的缓冲区。
 *
 *		我们读取页面并尝试获取清理锁。如果我们获得了锁，
 *		我们返回缓冲区；否则，我们返回 InvalidBuffer。
 */
Buffer _hash_getbuf_with_condlock_cleanup(Relation fc_rel, BlockNumber fc_blkno, int fc_flags)
{
	Buffer		fc_buf;

	if (fc_blkno == P_NEW)
		elog(ERROR, "hash AM does not use P_NEW");

	fc_buf = ReadBuffer(fc_rel, fc_blkno);

	if (!ConditionalLockBufferForCleanup(fc_buf))
	{
		ReleaseBuffer(fc_buf);
		return InvalidBuffer;
	}

	/* 引用计数和锁类型是正确的 */

	_hash_checkpage(fc_rel, fc_buf, fc_flags);

	return fc_buf;
}

/*
 *	_hash_getinitbuf() -- 通过块编号获取并初始化一个缓冲区。
 *
 *		这只能用于获取已知在索引的文件系统 EOF 之前的页面，但要从头开始填充。
 *		_hash_pageinit() 会自动应用。否则，它的效果类似于 access = HASH_WRITE 的 _hash_getbuf()。
 *
 *		当这个例程返回时，请求的缓冲区上会设置写锁，并且它的引用计数已递增
 *		（即，缓冲区是“已锁定并固定”）。
 *
 *		P_NEW 是不允许的，因为这个例程只能用于访问已知在文件系统 EOF 之前的页面。
 *		扩展索引应该使用 _hash_getnewbuf 完成。
 */
Buffer _hash_getinitbuf(Relation fc_rel, BlockNumber fc_blkno)
{
	Buffer		fc_buf;

	if (fc_blkno == P_NEW)
		elog(ERROR, "hash AM does not use P_NEW");

	fc_buf = ReadBufferExtended(fc_rel, MAIN_FORKNUM, fc_blkno, RBM_ZERO_AND_LOCK,
							 NULL);

	/* 引用计数和锁类型是正确的 */

	/* 初始化页面 */
	_hash_pageinit(BufferGetPage(fc_buf), BufferGetPageSize(fc_buf));

	return fc_buf;
}

/*
 *	_hash_initbuf() -- 通过桶编号获取并初始化一个缓冲区。
 */
void _hash_initbuf(Buffer fc_buf, uint32 fc_max_bucket, uint32 fc_num_bucket, uint32 fc_flag,
			  bool fc_initpage)
{
	HashPageOpaque fc_pageopaque;
	Page		fc_page;

	fc_page = BufferGetPage(fc_buf);

	/* 初始化页面 */
	if (fc_initpage)
		_hash_pageinit(fc_page, BufferGetPageSize(fc_buf));

	fc_pageopaque = HashPageGetOpaque(fc_page);

	/*
	 * 使用当前的 hashm_maxbucket 设置 hasho_prevblkno。这个值将用于验证缓存的 HashMetaPageData。
	 * 参见 _hash_getbucketbuf_from_hashkey()。
	 */
	fc_pageopaque->hasho_prevblkno = fc_max_bucket;
	fc_pageopaque->hasho_nextblkno = InvalidBlockNumber;
	fc_pageopaque->hasho_bucket = fc_num_bucket;
	fc_pageopaque->hasho_flag = fc_flag;
	fc_pageopaque->hasho_page_id = HASHO_PAGE_ID;
}

/*
 *	_hash_getnewbuf() -- 在索引的末尾获取一个新页面。
 *
 *		这与 _hash_getinitbuf 的 API 相同，只是我们要将页面添加到索引，
 *		因此期望该页面超出逻辑 EOF。（然而，我们必须支持不满足这一条件的情况，
 *		因为之前的尝试可能在扩展文件系统 EOF 后崩溃，但在更新元页面以反映添加页面之前。）
 *
 *		确保只有一个进程可以同时扩展索引是调用者的责任。实际上，这个函数
 *		仅在持有元页面的写锁时被调用，因为添加页面总是与元页面数据的更新相关联。
 */
Buffer _hash_getnewbuf(Relation fc_rel, BlockNumber fc_blkno, ForkNumber fc_forkNum)
{
	BlockNumber fc_nblocks = RelationGetNumberOfBlocksInFork(fc_rel, fc_forkNum);
	Buffer		fc_buf;

	if (fc_blkno == P_NEW)
		elog(ERROR, "hash AM does not use P_NEW");
	if (fc_blkno > fc_nblocks)
		elog(ERROR, "access to noncontiguous page in hash index \"%s\"",
			 RelationGetRelationName(fc_rel));

	/* smgr 强制我们使用 P_NEW 扩展关系 */
	if (fc_blkno == fc_nblocks)
	{
		fc_buf = ReadBufferExtended(fc_rel, fc_forkNum, P_NEW, RBM_NORMAL, NULL);
		if (BufferGetBlockNumber(fc_buf) != fc_blkno)
			elog(ERROR, "unexpected hash relation size: %u, should be %u",
				 BufferGetBlockNumber(fc_buf), fc_blkno);
		LockBuffer(fc_buf, HASH_WRITE);
	}
	else
	{
		fc_buf = ReadBufferExtended(fc_rel, fc_forkNum, fc_blkno, RBM_ZERO_AND_LOCK,
								 NULL);
	}

	/* 引用计数和锁类型是正确的 */

	/* 初始化页面 */
	_hash_pageinit(BufferGetPage(fc_buf), BufferGetPageSize(fc_buf));

	return fc_buf;
}

/*
 *	_hash_getbuf_with_strategy() -- 获取具有非默认策略的缓冲区。
 *
 *		这与 _hash_getbuf() 相同，但也允许指定缓冲区访问
 *		策略。我们在 VACUUM 操作中使用它。
 */
Buffer _hash_getbuf_with_strategy(Relation fc_rel, BlockNumber fc_blkno,
						   int fc_access, int fc_flags,
						   BufferAccessStrategy fc_bstrategy)
{
	Buffer		fc_buf;

	if (fc_blkno == P_NEW)
		elog(ERROR, "hash AM does not use P_NEW");

	fc_buf = ReadBufferExtended(fc_rel, MAIN_FORKNUM, fc_blkno, RBM_NORMAL, fc_bstrategy);

	if (fc_access != HASH_NOLOCK)
		LockBuffer(fc_buf, fc_access);

	/* 引用计数和锁类型是正确的 */

	_hash_checkpage(fc_rel, fc_buf, fc_flags);

	return fc_buf;
}

/*
 *	_hash_relbuf() -- 释放锁定的缓冲区。
 *
 * 锁和引脚（引用计数）都会被释放。
 */
void _hash_relbuf(Relation fc_rel, Buffer fc_buf)
{
	UnlockReleaseBuffer(fc_buf);
}

/*
 *	_hash_dropbuf() -- 释放未加锁的缓冲区。
 *
 * 这用于卸下我们没有持有锁的缓冲区。
 */
void _hash_dropbuf(Relation fc_rel, Buffer fc_buf)
{
	ReleaseBuffer(fc_buf);
}

/*
 *	_hash_dropscanbuf() -- 释放在扫描中使用的缓冲区。
 *
 * 此例程解除在扫描过程中使用的缓冲区的引脚，
 * 我们对其没有持有锁。
 */
void _hash_dropscanbuf(Relation fc_rel, HashScanOpaque fc_so)
{
	/* 释放我们在主桶页面上持有的引脚 */
	if (BufferIsValid(fc_so->hashso_bucket_buf) &&
		fc_so->hashso_bucket_buf != fc_so->currPos.buf)
		_hash_dropbuf(fc_rel, fc_so->hashso_bucket_buf);
	fc_so->hashso_bucket_buf = InvalidBuffer;

	/* 释放我们在被分割的桶的主桶页面上持有的引脚 */
	if (BufferIsValid(fc_so->hashso_split_bucket_buf) &&
		fc_so->hashso_split_bucket_buf != fc_so->currPos.buf)
		_hash_dropbuf(fc_rel, fc_so->hashso_split_bucket_buf);
	fc_so->hashso_split_bucket_buf = InvalidBuffer;

	/* 释放我们仍然持有的任何引脚 */
	if (BufferIsValid(fc_so->currPos.buf))
		_hash_dropbuf(fc_rel, fc_so->currPos.buf);
	fc_so->currPos.buf = InvalidBuffer;

	/* 重置分割扫描 */
	fc_so->hashso_buc_populated = false;
	fc_so->hashso_buc_split = false;
}


/*
 *	_hash_init() -- 初始化哈希索引的元数据页面，
 *			 初始桶和初始位图页面。
 *
 * 初始桶的数量依赖于 num_tuples，这是对
 * 初始要加载到索引中的元组数量的估计。
 * 返还选定的桶数。
 *
 * 我们在这里对锁的处理相当随意，因为我们知道没有其他人
 * 可以访问这个索引。特别是，对于不持有多个缓冲区锁的规则
 * 被忽略了。
 */
uint32 _hash_init(Relation fc_rel, double fc_num_tuples, ForkNumber fc_forkNum)
{
	Buffer		fc_metabuf;
	Buffer		fc_buf;
	Buffer		fc_bitmapbuf;
	Page		fc_pg;
	HashMetaPage fc_metap;
	RegProcedure fc_procid;
	int32		fc_data_width;
	int32		fc_item_width;
	int32		fc_ffactor;
	uint32		fc_num_buckets;
	uint32		fc_i;
	bool		fc_use_wal;

	/* 安全检查 */
	if (RelationGetNumberOfBlocksInFork(fc_rel, fc_forkNum) != 0)
		elog(ERROR, "cannot initialize non-empty hash index \"%s\"",
			 RelationGetRelationName(fc_rel));

	/*
	 * 如果关系是持久的，或者这是初始化分支，则 WAL 记录页的创建。
	 * 对于未记录的关系，初始化分支始终需要 WAL 记录。
	 */
	fc_use_wal = RelationNeedsWAL(fc_rel) || fc_forkNum == INIT_FORKNUM;

	/*
	 * 确定此索引的目标填充因子（每个桶的元组数）。
	 * 这个想法是使填充因子对应于用户可设置的
	 * fillfactor 参数所说的页的满度。由于索引数据类型（即
	 * uint32 哈希键）是固定宽度的，我们可以精确计算它。
	 */
	fc_data_width = sizeof(uint32);
	fc_item_width = MAXALIGN(sizeof(IndexTupleData)) + MAXALIGN(fc_data_width) +
		sizeof(ItemIdData);		/* 包括行指针 */
	fc_ffactor = HashGetTargetPageUsage(fc_rel) / fc_item_width;
	/* 保持在理智范围内 */
	if (fc_ffactor < 10)
		fc_ffactor = 10;

	fc_procid = index_getprocid(fc_rel, 1, HASHSTANDARD_PROC);

	/*
	 * 我们顺序初始化元页面、前 N 个桶页面和第一个
	 * 位图页面，使用 _hash_getnewbuf 触发 smgrextend()
	 * 调用。这确保了 smgr 层对
	 * 物理索引长度有正确的认识。
	 *
	 * 不需要临界区，因为在错误发生时整个关系的创建将被回滚。
	 */
	fc_metabuf = _hash_getnewbuf(fc_rel, HASH_METAPAGE, fc_forkNum);
	_hash_init_metabuffer(fc_metabuf, fc_num_tuples, fc_procid, fc_ffactor, false);
	MarkBufferDirty(fc_metabuf);

	fc_pg = BufferGetPage(fc_metabuf);
	fc_metap = HashPageGetMeta(fc_pg);

	/* XLOG 相关内容 */
	if (fc_use_wal)
	{
		xl_hash_init_meta_page fc_xlrec;
		XLogRecPtr	fc_recptr;

		fc_xlrec.num_tuples = fc_num_tuples;
		fc_xlrec.procid = fc_metap->hashm_procid;
		fc_xlrec.ffactor = fc_metap->hashm_ffactor;

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfHashInitMetaPage);
		XLogRegisterBuffer(0, fc_metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD);

		fc_recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_INIT_META_PAGE);

		PageSetLSN(BufferGetPage(fc_metabuf), fc_recptr);
	}

	fc_num_buckets = fc_metap->hashm_maxbucket + 1;

	/*
	 * 在初始化桶时释放元页面上的缓冲区锁。
	 * 否则，我们将处于中断保持状态，CHECK_FOR_INTERRUPTS
	 * 不会有什么效果。在任何情况下，长时间持有缓冲区锁都是
	 * 不好的，因为这会阻塞 bgwriter。
	 */
	LockBuffer(fc_metabuf, BUFFER_LOCK_UNLOCK);

	/*
	 * 初始化并 WAL 记录前 N 个桶
	 */
	for (fc_i = 0; fc_i < fc_num_buckets; fc_i++)
	{
		BlockNumber fc_blkno;

		/* 允许中断，以防 N 非常大 */
		CHECK_FOR_INTERRUPTS();

		fc_blkno = BUCKET_TO_BLKNO(fc_metap, fc_i);
		fc_buf = _hash_getnewbuf(fc_rel, fc_blkno, fc_forkNum);
		_hash_initbuf(fc_buf, fc_metap->hashm_maxbucket, fc_i, LH_BUCKET_PAGE, false);
		MarkBufferDirty(fc_buf);

		if (fc_use_wal)
			log_newpage(&fc_rel->rd_node,
						fc_forkNum,
						fc_blkno,
						BufferGetPage(fc_buf),
						true);
		_hash_relbuf(fc_rel, fc_buf);
	}

	/* 现在重新获取元页面上的缓冲区锁 */
	LockBuffer(fc_metabuf, BUFFER_LOCK_EXCLUSIVE);

	/*
	 * 初始化位图页面
	 */
	fc_bitmapbuf = _hash_getnewbuf(fc_rel, fc_num_buckets + 1, fc_forkNum);
	_hash_initbitmapbuffer(fc_bitmapbuf, fc_metap->hashm_bmsize, false);
	MarkBufferDirty(fc_bitmapbuf);

	/* 将新位图页面添加到元页面的位图列表中 */
	/* 元页面已经有写锁 */
	if (fc_metap->hashm_nmaps >= HASH_MAX_BITMAPS)
		ereport(ERROR,
				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
				 errmsg("out of overflow pages in hash index \"%s\"",
						RelationGetRelationName(fc_rel))));

	fc_metap->hashm_mapp[fc_metap->hashm_nmaps] = fc_num_buckets + 1;

	fc_metap->hashm_nmaps++;
	MarkBufferDirty(fc_metabuf);

	/* XLOG 相关内容 */
	if (fc_use_wal)
	{
		xl_hash_init_bitmap_page fc_xlrec;
		XLogRecPtr	fc_recptr;

		fc_xlrec.bmsize = fc_metap->hashm_bmsize;

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfHashInitBitmapPage);
		XLogRegisterBuffer(0, fc_bitmapbuf, REGBUF_WILL_INIT);

		/*
		 * 这只有在没有其他人可以在这个阶段修改索引的情况下才是安全的；它仅对正在创建它的事务可见。
		 */
		XLogRegisterBuffer(1, fc_metabuf, REGBUF_STANDARD);

		fc_recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_INIT_BITMAP_PAGE);

		PageSetLSN(BufferGetPage(fc_bitmapbuf), fc_recptr);
		PageSetLSN(BufferGetPage(fc_metabuf), fc_recptr);
	}

	/* 全部完成 */
	_hash_relbuf(fc_rel, fc_bitmapbuf);
	_hash_relbuf(fc_rel, fc_metabuf);

	return fc_num_buckets;
}

/*
 *	_hash_init_metabuffer() -- 初始化哈希索引的元数据页面。
 */
void _hash_init_metabuffer(Buffer fc_buf, double fc_num_tuples, RegProcedure fc_procid,
					  uint16 fc_ffactor, bool fc_initpage)
{
	HashMetaPage fc_metap;
	HashPageOpaque fc_pageopaque;
	Page		fc_page;
	double		fc_dnumbuckets;
	uint32		fc_num_buckets;
	uint32		fc_spare_index;
	uint32		fc_lshift;

	/*
	 * 根据估计的元组数量选择初始桶页面的数量以匹配填充因子。我们将结果向上舍入到
	 * 必须在使用其 hashm_spares 元素之前分配的桶的总数。然而，始终强制至少有 2 页桶。
	 * 上限由在 _hash_expandtable() 中解释的考虑决定。
	 */
	fc_dnumbuckets = fc_num_tuples / fc_ffactor;
	if (fc_dnumbuckets <= 2.0)
		fc_num_buckets = 2;
	else if (fc_dnumbuckets >= (double) 0x40000000)
		fc_num_buckets = 0x40000000;
	else
		fc_num_buckets = _hash_get_totalbuckets(_hash_spareindex(fc_dnumbuckets));

	fc_spare_index = _hash_spareindex(fc_num_buckets);
	Assert(fc_spare_index < HASH_MAX_SPLITPOINTS);

	fc_page = BufferGetPage(fc_buf);
	if (fc_initpage)
		_hash_pageinit(fc_page, BufferGetPageSize(fc_buf));

	fc_pageopaque = HashPageGetOpaque(fc_page);
	fc_pageopaque->hasho_prevblkno = InvalidBlockNumber;
	fc_pageopaque->hasho_nextblkno = InvalidBlockNumber;
	fc_pageopaque->hasho_bucket = InvalidBucket;
	fc_pageopaque->hasho_flag = LH_META_PAGE;
	fc_pageopaque->hasho_page_id = HASHO_PAGE_ID;

	fc_metap = HashPageGetMeta(fc_page);

	fc_metap->hashm_magic = HASH_MAGIC;
	fc_metap->hashm_version = HASH_VERSION;
	fc_metap->hashm_ntuples = 0;
	fc_metap->hashm_nmaps = 0;
	fc_metap->hashm_ffactor = fc_ffactor;
	fc_metap->hashm_bsize = HashGetMaxBitmapSize(fc_page);

	/* 找到适合页面大小的最大位图数组大小 */
	fc_lshift = pg_leftmost_one_pos32(fc_metap->hashm_bsize);
	Assert(fc_lshift > 0);
	fc_metap->hashm_bmsize = 1 << fc_lshift;
	fc_metap->hashm_bmshift = fc_lshift + BYTE_TO_BIT;
	Assert((1 << BMPG_SHIFT(fc_metap)) == (BMPG_MASK(fc_metap) + 1));

	/*
	 * 用其主要哈希支持函数的 OID 来标记索引。这对正常操作来说几乎没有用（实际上，hashm_procid
	 * 在任何地方都没有使用），但它可能对取证目的有用，所以我们保留它。
	 */
	fc_metap->hashm_procid = fc_procid;

	/*
	 * 我们用 N 个桶初始化索引，0..N-1，占用物理块 1 到 N。第一个空闲位图页面位于块 N+1。
	 */
	fc_metap->hashm_maxbucket = fc_num_buckets - 1;

	/*
	 * 将高掩码设置为下一个立即的 ((2 ^ x) - 1)，这应该足以覆盖 num_buckets。
	 */
	fc_metap->hashm_highmask = pg_nextpower2_32(fc_num_buckets + 1) - 1;
	fc_metap->hashm_lowmask = (fc_metap->hashm_highmask >> 1);

	MemSet(fc_metap->hashm_spares, 0, sizeof(fc_metap->hashm_spares));
	MemSet(fc_metap->hashm_mapp, 0, sizeof(fc_metap->hashm_mapp));

	/* 在初始拆分点后设置一个备用页面的映射 */
	fc_metap->hashm_spares[fc_spare_index] = 1;
	fc_metap->hashm_ovflpoint = fc_spare_index;
	fc_metap->hashm_firstfree = 0;

	/*
	 * 将 pd_lower 设置在元数据结束的后面。这是至关重要的，因为不这样做，如果 xlog.c 压缩页面，元数据将丢失。
	 */
	((PageHeader) fc_page)->pd_lower =
		((char *) fc_metap + sizeof(HashMetaPageData)) - (char *) fc_page;
}

/*
 *	_hash_pageinit() -- 初始化新的哈希索引页面。
 */
void _hash_pageinit(Page fc_page, Size fc_size)
{
	PageInit(fc_page, fc_size, sizeof(HashPageOpaqueData));
}

/*
 * 尝试通过创建一个新桶来扩展哈希表。
 *
 * 如果我们没有在旧桶或新桶上获得清理锁，这将默默无闻地无所作为。
 *
 * 完成待处理的拆分并从旧桶中移除元组，如果有从上一个拆分中剩余的元组。
 *
 * 调用者必须在元页面缓冲区上保持一个锁定，但不需要锁定。
 * 缓冲区以相同的状态返回。
 */
void _hash_expandtable(Relation fc_rel, Buffer fc_metabuf)
{
	HashMetaPage fc_metap;
	Bucket		fc_old_bucket;
	Bucket		fc_new_bucket;
	uint32		fc_spare_ndx;
	BlockNumber fc_start_oblkno;
	BlockNumber fc_start_nblkno;
	Buffer		fc_buf_nblkno;
	Buffer		fc_buf_oblkno;
	Page		fc_opage;
	Page		fc_npage;
	HashPageOpaque fc_oopaque;
	HashPageOpaque fc_nopaque;
	uint32		fc_maxbucket;
	uint32		fc_highmask;
	uint32		fc_lowmask;
	bool		fc_metap_update_masks = false;
	bool		fc_metap_update_splitpoint = false;

restart_expand:

	/*
	 * 写锁定元页面。以前开始拆分必须获取一个重量级锁，但现在不再需要。
	 */
	LockBuffer(fc_metabuf, BUFFER_LOCK_EXCLUSIVE);

	_hash_checkpage(fc_rel, fc_metabuf, LH_META_PAGE);
	fc_metap = HashPageGetMeta(BufferGetPage(fc_metabuf));

	/*
	 * 检查是否仍然需要拆分；在我们等待锁的时候可能其他人已经完成了一次拆分。
	 *
	 * 确保这与 _hash_doinsert() 保持同步。
	 */
	if (fc_metap->hashm_ntuples <=
		(double) fc_metap->hashm_ffactor * (fc_metap->hashm_maxbucket + 1))
		goto fail;

	/*
	 * 如果 maxbucket 达到其最大可能值，就不能再拆分了。
	 *
	 * 理想情况下，我们希望允许桶编号达到 UINT_MAX-1（不再高，因为计算 maxbucket+1
	 * 不能溢出）。目前我们将限制为其一半，以防止 pg_ceil_log2_32() 失败和 hashm_spares[]
	 * 空间不足。无论如何这是无关紧要的，因为一个具有 2^32 个桶的索引肯定会溢出 BlockNumber
	 * ，因此 _hash_alloc_buckets() 将失败，但如果我们支持小于磁盘块的桶，那么这将是一个独立的约束。
	 *
	 * 如果你改变这个，请查看 _hash_init() 中桶的最大初始数量。
	 */
	if (fc_metap->hashm_maxbucket >= (uint32) 0x7FFFFFFE)
		goto fail;

	/*
	 * 确定要拆分哪个桶，并尝试在旧桶上获取清理锁。如果我们无法获取锁，则放弃。
	 *
	 * 清理锁不仅保护我们免受其他后端的影响，还保护我们自己的后端。
	 *
	 * 清理锁主要是为了保护拆分过程免受并发插入的影响。有关更多详细信息，请参见src/backend/access/hash/README，锁定义。由于这种锁定限制，如果有任何待处理的扫描，则拆分将放弃，这虽然不好，但无害。
	 */
	fc_new_bucket = fc_metap->hashm_maxbucket + 1;

	fc_old_bucket = (fc_new_bucket & fc_metap->hashm_lowmask);

	fc_start_oblkno = BUCKET_TO_BLKNO(fc_metap, fc_old_bucket);

	fc_buf_oblkno = _hash_getbuf_with_condlock_cleanup(fc_rel, fc_start_oblkno, LH_BUCKET_PAGE);
	if (!fc_buf_oblkno)
		goto fail;

	fc_opage = BufferGetPage(fc_buf_oblkno);
	fc_oopaque = HashPageGetOpaque(fc_opage);

	/*
	 * 我们希望完成从一个桶的拆分，因为不这样做显然没有任何好处，并且考虑到新的拆分也失败的情况，完成涉及多个桶的拆分将使代码变得复杂。我们这里不需要考虑新桶，因为当旧桶仍有待处理的拆分时，不可能开始对新桶进行重新拆分。
	 */
	if (H_BUCKET_BEING_SPLIT(fc_oopaque))
	{
		/*
		 * 现在复制桶映射信息；请参阅下面代码中的注释，我们在调用_hash_splitbucket之前复制该信息，以查看这为什么可以。
		 */
		fc_maxbucket = fc_metap->hashm_maxbucket;
		fc_highmask = fc_metap->hashm_highmask;
		fc_lowmask = fc_metap->hashm_lowmask;

		/*
		 * 在完成拆分之前，释放对metapage和old_bucket的锁。
		 */
		LockBuffer(fc_metabuf, BUFFER_LOCK_UNLOCK);
		LockBuffer(fc_buf_oblkno, BUFFER_LOCK_UNLOCK);

		_hash_finish_split(fc_rel, fc_metabuf, fc_buf_oblkno, fc_old_bucket, fc_maxbucket,
						   fc_highmask, fc_lowmask);

		/* 释放对旧缓冲区的固定并重试扩展。 */
		_hash_dropbuf(fc_rel, fc_buf_oblkno);

		goto restart_expand;
	}

	/*
	 * 清理上一拆分中剩余的元组。此操作需要清理锁，我们已经在旧桶上获取了一个锁，所以让我们执行此操作。在清理之前的拆分垃圾之前，我们也不希望允许进一步的拆分。这有两个好处；首先，它有助于避免由于垃圾造成的膨胀，其次是在清理桶期间，我们始终可以确定垃圾元组属于最近拆分的桶。相反，如果我们在元数据页面更新以指示新拆分之后而实际拆分之前，允许清理桶，则清理操作将无法判断元组是否已移动到新创建的桶，从而最终删除此类元组。
	 */
	if (H_NEEDS_SPLIT_CLEANUP(fc_oopaque))
	{
		/*
		 * 现在复制桶映射信息；请参阅下面代码中的注释，我们在调用_hash_splitbucket之前复制该信息，以查看这为什么可以。
		 */
		fc_maxbucket = fc_metap->hashm_maxbucket;
		fc_highmask = fc_metap->hashm_highmask;
		fc_lowmask = fc_metap->hashm_lowmask;

		/* 释放metapage锁。 */
		LockBuffer(fc_metabuf, BUFFER_LOCK_UNLOCK);

		hashbucketcleanup(fc_rel, fc_old_bucket, fc_buf_oblkno, fc_start_oblkno, NULL,
						  fc_maxbucket, fc_highmask, fc_lowmask, NULL, NULL, true,
						  NULL, NULL);

		_hash_dropbuf(fc_rel, fc_buf_oblkno);

		goto restart_expand;
	}

	/*
	 * 新桶上不应有任何活动扫描。
	 *
	 * 注意：尽管我们可能仍需更新BUCKET_TO_BLKNO映射，但在这里计算新桶的blkno是安全的。这是因为hashm_spares[hashm_ovflpoint]的当前值正确显示了我们将把新拆分点的一系列桶放置到哪里。
	 */
	fc_start_nblkno = BUCKET_TO_BLKNO(fc_metap, fc_new_bucket);

	/*
	 * 如果拆分点在增加，我们需要分配一批新的桶页面。
	 */
	fc_spare_ndx = _hash_spareindex(fc_new_bucket + 1);
	if (fc_spare_ndx > fc_metap->hashm_ovflpoint)
	{
		uint32		fc_buckets_to_add;

		Assert(fc_spare_ndx == fc_metap->hashm_ovflpoint + 1);

		/*
		 * 我们将桶的分配视为一个单独的 WAL 记录操作。
		 * 即使在这个操作后发生故障，也不会泄漏桶页面；
		 * 而是下一个拆分将使用这个空间。无论如何，即使
		 * 没有故障，我们在一次拆分操作中也不会使用所有空间。
		 */
		fc_buckets_to_add = _hash_get_totalbuckets(fc_spare_ndx) - fc_new_bucket;
		if (!fc__hash_alloc_buckets(fc_rel, fc_start_nblkno, fc_buckets_to_add))
		{
			/* 由于 BlockNumber 溢出无法拆分 */
			_hash_relbuf(fc_rel, fc_buf_oblkno);
			goto fail;
		}
	}

	/*
	 * 物理分配新桶的主页面。我们希望在更改元页面的映射信息之前执行此操作，以防我们无法获得
	 * 磁盘空间。
	 *
	 * XXX 先调用 _hash_getnewbuf 然后将缓冲区归零，然后才检查我们是否有清理锁，
	 * 这并没有意义。然而，由于还没有扫描可以访问缓冲区，因此任何并发
	 * 访问将仅来自 bgwriter 或检查点进程等，这些进程不关心其内容，因此这并不重要。
	 */
	fc_buf_nblkno = _hash_getnewbuf(fc_rel, fc_start_nblkno, MAIN_FORKNUM);
	if (!IsBufferCleanupOK(fc_buf_nblkno))
	{
		_hash_relbuf(fc_rel, fc_buf_oblkno);
		_hash_relbuf(fc_rel, fc_buf_nblkno);
		goto fail;
	}

	/*
	 * 由于我们在共享缓冲区上进行编辑，因此建立一个临界区。
	 * 在下一个代码中的任何故障都会给我们带来一个大问题：元页面实际上是损坏的，但可能会被写回
	 * 磁盘。
	 */
	START_CRIT_SECTION();

	/*
	 * 可以继续进行拆分。更新元页面的桶映射信息。
	 */
	fc_metap->hashm_maxbucket = fc_new_bucket;

	if (fc_new_bucket > fc_metap->hashm_highmask)
	{
		/* 开始新的翻倍 */
		fc_metap->hashm_lowmask = fc_metap->hashm_highmask;
		fc_metap->hashm_highmask = fc_new_bucket | fc_metap->hashm_lowmask;
		fc_metap_update_masks = true;
	}

	/*
	 * 如果拆分点在增加，我们需要调整 hashm_spares[]
	 * 数组和 hashm_ovflpoint，以便未来的溢出页面将在这批新桶页面之后创建。
	 */
	if (fc_spare_ndx > fc_metap->hashm_ovflpoint)
	{
		fc_metap->hashm_spares[fc_spare_ndx] = fc_metap->hashm_spares[fc_metap->hashm_ovflpoint];
		fc_metap->hashm_ovflpoint = fc_spare_ndx;
		fc_metap_update_splitpoint = true;
	}

	MarkBufferDirty(fc_metabuf);

	/*
	 * 现在复制桶映射信息；这可以节省在 _hash_splitbucket 内部循环中重新访问元页面的时间。
	 * 请注意，一旦我们放弃拆分锁，其他拆分可能会开始，因此这些值可能会在 _hash_splitbucket
	 * 完成之前过时。这没关系，因为它只需要告诉哪些桶将映射哈希键。
	 */
	fc_maxbucket = fc_metap->hashm_maxbucket;
	fc_highmask = fc_metap->hashm_highmask;
	fc_lowmask = fc_metap->hashm_lowmask;

	fc_opage = BufferGetPage(fc_buf_oblkno);
	fc_oopaque = HashPageGetOpaque(fc_opage);

	/*
	 * 标记旧桶以指示拆分正在进行中。 （在操作结束时，我们将清除拆分进行中的标志。）
	 * 此外，对于主桶页面，hasho_prevblkno 存储上一次拆分时存在的桶数量，
	 * 因此我们必须在这里更新该值。
	 */
	fc_oopaque->hasho_flag |= LH_BUCKET_BEING_SPLIT;
	fc_oopaque->hasho_prevblkno = fc_maxbucket;

	MarkBufferDirty(fc_buf_oblkno);

	fc_npage = BufferGetPage(fc_buf_nblkno);

	/*
	 * 初始化新桶的主页面并标记以指示拆分正在进行中。
	 */
	fc_nopaque = HashPageGetOpaque(fc_npage);
	fc_nopaque->hasho_prevblkno = fc_maxbucket;
	fc_nopaque->hasho_nextblkno = InvalidBlockNumber;
	fc_nopaque->hasho_bucket = fc_new_bucket;
	fc_nopaque->hasho_flag = LH_BUCKET_PAGE | LH_BUCKET_BEING_POPULATED;
	fc_nopaque->hasho_page_id = HASHO_PAGE_ID;

	MarkBufferDirty(fc_buf_nblkno);

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_rel))
	{
		xl_hash_split_allocate_page fc_xlrec;
		XLogRecPtr	fc_recptr;

		fc_xlrec.new_bucket = fc_maxbucket;
		fc_xlrec.old_bucket_flag = fc_oopaque->hasho_flag;
		fc_xlrec.new_bucket_flag = fc_nopaque->hasho_flag;
		fc_xlrec.flags = 0;

		XLogBeginInsert();

		XLogRegisterBuffer(0, fc_buf_oblkno, REGBUF_STANDARD);
		XLogRegisterBuffer(1, fc_buf_nblkno, REGBUF_WILL_INIT);
		XLogRegisterBuffer(2, fc_metabuf, REGBUF_STANDARD);

		if (fc_metap_update_masks)
		{
			fc_xlrec.flags |= XLH_SPLIT_META_UPDATE_MASKS;
			XLogRegisterBufData(2, (char *) &fc_metap->hashm_lowmask, sizeof(uint32));
			XLogRegisterBufData(2, (char *) &fc_metap->hashm_highmask, sizeof(uint32));
		}

		if (fc_metap_update_splitpoint)
		{
			fc_xlrec.flags |= XLH_SPLIT_META_UPDATE_SPLITPOINT;
			XLogRegisterBufData(2, (char *) &fc_metap->hashm_ovflpoint,
								sizeof(uint32));
			XLogRegisterBufData(2,
								(char *) &fc_metap->hashm_spares[fc_metap->hashm_ovflpoint],
								sizeof(uint32));
		}

		XLogRegisterData((char *) &fc_xlrec, SizeOfHashSplitAllocPage);

		fc_recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_SPLIT_ALLOCATE_PAGE);

		PageSetLSN(BufferGetPage(fc_buf_oblkno), fc_recptr);
		PageSetLSN(BufferGetPage(fc_buf_nblkno), fc_recptr);
		PageSetLSN(BufferGetPage(fc_metabuf), fc_recptr);
	}

	END_CRIT_SECTION();

	/* 放弃锁定，但保持引脚 */
	LockBuffer(fc_metabuf, BUFFER_LOCK_UNLOCK);

	/* 将记录重定位到新桶 */
	fc__hash_splitbucket(fc_rel, fc_metabuf,
					  fc_old_bucket, fc_new_bucket,
					  fc_buf_oblkno, fc_buf_nblkno, NULL,
					  fc_maxbucket, fc_highmask, fc_lowmask);

	/* 所有操作已完成，现在释放对主桶的引脚。 */
	_hash_dropbuf(fc_rel, fc_buf_oblkno);
	_hash_dropbuf(fc_rel, fc_buf_nblkno);

	return;

	/* 如果决定不拆分或未能获取旧桶锁，则到这里 */
fail:

	/* 我们没有写入元页面，所以只需放弃锁定 */
	LockBuffer(fc_metabuf, BUFFER_LOCK_UNLOCK);
}


/*
 * _hash_alloc_buckets -- 分配一个新的分割点所需的桶页面
 *
 * 这不需要初始化新的桶页面；我们将在每个页面被_hash_expandtable()使用时进行初始化。
 * 但是我们必须将逻辑EOF扩展到分割点的末尾；这将保持smgr对EOF的理解与我们的同步，
 * 以便我们不会收到来自smgr的抱怨。
 *
 * 我们通过在分割点范围的末尾写入一页零来实现这一点。
 * 我们预计文件系统也会确保中间页面读取为零。
 * 在许多文件系统中，这个“洞”不会立即分配，这意味着索引文件可能会比我们强制立即分配所有页面时更碎片化；
 * 但由于我们反正不按顺序扫描哈希索引，这可能并不重要。
 *
 * XXX 让人烦恼的是，这段代码在持有元页面锁的情况下执行。
 * 我们需要对_hash_addovflpage()并发添加新溢出页面进行互锁，
 * 但对于此目的，使用LockRelationForExtension可能会更好。另一方面，
 * 添加一个分割点是一个非常不频繁的操作，因此可能不值得担心。
 *
 * 如果成功则返回true；如果由于BlockNumber溢出而分配失败则返回false。
 */
static bool fc__hash_alloc_buckets(Relation fc_rel, BlockNumber fc_firstblock, uint32 fc_nblocks)
{
	BlockNumber fc_lastblock;
	PGAlignedBlock fc_zerobuf;
	Page		fc_page;
	HashPageOpaque fc_ovflopaque;

	fc_lastblock = fc_firstblock + fc_nblocks - 1;

	/*
	 * 检查块号计算中的溢出；如果是，我们不能再扩展索引。
	 */
	if (fc_lastblock < fc_firstblock || fc_lastblock == InvalidBlockNumber)
		return false;

	fc_page = (Page) fc_zerobuf.data;

	/*
	 * 初始化页面。仅仅将页面置为零是行不通的；请参见_hash_freeovflpage以获取类似用法。
	 * 我们确保特殊空间对诸如pageinspect等工具有效。
	 */
	_hash_pageinit(fc_page, BLCKSZ);

	fc_ovflopaque = HashPageGetOpaque(fc_page);

	fc_ovflopaque->hasho_prevblkno = InvalidBlockNumber;
	fc_ovflopaque->hasho_nextblkno = InvalidBlockNumber;
	fc_ovflopaque->hasho_bucket = InvalidBucket;
	fc_ovflopaque->hasho_flag = LH_UNUSED_PAGE;
	fc_ovflopaque->hasho_page_id = HASHO_PAGE_ID;

	if (RelationNeedsWAL(fc_rel))
		log_newpage(&fc_rel->rd_node,
					MAIN_FORKNUM,
					fc_lastblock,
					fc_zerobuf.data,
					true);

	PageSetChecksumInplace(fc_page, fc_lastblock);
	smgrextend(RelationGetSmgr(fc_rel), MAIN_FORKNUM, fc_lastblock, fc_zerobuf.data,
			   false);

	return true;
}


/*
 * _hash_splitbucket -- 将 'obucket' 拆分为 'obucket' 和 'nbucket'
 *
 * 此例程用于在旧桶和新桶之间分区元组，并用于完成未完成的拆分操作。
 * 要完成先前中断的拆分操作，调用者需要填充 htab。如果 htab 已设置，
 * 则我们跳过在 htab 中存在的元组的移动，否则 htab 的 NULL 值表示移动
 * 所有属于新桶的元组。
 *
 * 我们正在拆分一个包含基本桶页面和零个或多个溢出（桶链）页面的桶。
 * 我们必须重新定位属于新桶的元组。
 *
 * 调用者必须在两个桶上持有清理锁，以确保没有其他人试图访问它们（见 README）。
 *
 * 调用者必须在元页面缓冲区上持有固定，但不需要锁定。
 * 缓冲区将在相同状态下返回。（仅在必须添加或移除溢出页面时
 * 才会触及元页面。）
 *
 * 拆分需要在旧桶和新桶的主要桶页面上保持固定，直到操作结束。
 * 这是为了防止在拆分进行时开始清空。
 *
 * 此外，调用者必须已经创建新桶的基本页面，该页面在缓冲区 nbuf 中传入，
 * 被固定并写锁定。锁将在此处释放，固定必须由调用者释放。
 * （API 是这样设置的，因为我们必须在释放元页面写锁之前执行
 * _hash_getnewbuf()。因此，我们传递的是实际缓冲区，而不是
 * 新桶的起始块编号。）
 */
static void fc__hash_splitbucket(Relation fc_rel,
				  Buffer fc_metabuf,
				  Bucket fc_obucket,
				  Bucket fc_nbucket,
				  Buffer fc_obuf,
				  Buffer fc_nbuf,
				  HTAB *fc_htab,
				  uint32 fc_maxbucket,
				  uint32 fc_highmask,
				  uint32 fc_lowmask)
{
	Buffer		fc_bucket_obuf;
	Buffer		fc_bucket_nbuf;
	Page		fc_opage;
	Page		fc_npage;
	HashPageOpaque fc_oopaque;
	HashPageOpaque fc_nopaque;
	OffsetNumber fc_itup_offsets[MaxIndexTuplesPerPage];
	IndexTuple	fc_itups[MaxIndexTuplesPerPage];
	Size		fc_all_tups_size = 0;
	int			fc_i;
	uint16		fc_nitups = 0;

	fc_bucket_obuf = fc_obuf;
	fc_opage = BufferGetPage(fc_obuf);
	fc_oopaque = HashPageGetOpaque(fc_opage);

	fc_bucket_nbuf = fc_nbuf;
	fc_npage = BufferGetPage(fc_nbuf);
	fc_nopaque = HashPageGetOpaque(fc_npage);

	/* 将谓词锁从旧桶复制到新桶。 */
	PredicateLockPageSplit(fc_rel,
						   BufferGetBlockNumber(fc_bucket_obuf),
						   BufferGetBlockNumber(fc_bucket_nbuf));

	/*
	 * 在旧桶中对元组进行分区，在旧桶和新桶之间，
	 * 沿着旧桶的溢出桶链前进，并根据需要将溢出页面添加到新桶。
	 * 外层循环针对旧桶中的每个页面进行一次迭代。
	 */
	for (;;)
	{
		BlockNumber fc_oblkno;
		OffsetNumber fc_ooffnum;
		OffsetNumber fc_omaxoffnum;

		/* 扫描旧页面中的每个元组 */
		fc_omaxoffnum = PageGetMaxOffsetNumber(fc_opage);
		for (fc_ooffnum = FirstOffsetNumber;
			 fc_ooffnum <= fc_omaxoffnum;
			 fc_ooffnum = OffsetNumberNext(fc_ooffnum))
		{
			IndexTuple	fc_itup;
			Size		fc_itemsz;
			Bucket		fc_bucket;
			bool		fc_found = false;

			/* 跳过死元组 */
			if (ItemIdIsDead(PageGetItemId(fc_opage, fc_ooffnum)))
				continue;

			/*
			 * 在插入元组之前，探测包含属于新桶的元组的 TID 的哈希表，
			 * 如果找到匹配，则跳过该元组，否则获取项的哈希键（方便
			 * 存储在项中）并确定它现在属于哪个桶。
			 */
			fc_itup = (IndexTuple) PageGetItem(fc_opage,
											PageGetItemId(fc_opage, fc_ooffnum));

			if (fc_htab)
				(void) hash_search(fc_htab, &fc_itup->t_tid, HASH_FIND, &fc_found);

			if (fc_found)
				continue;

			fc_bucket = _hash_hashkey2bucket(_hash_get_indextuple_hashkey(fc_itup),
										  fc_maxbucket, fc_highmask, fc_lowmask);

			if (fc_bucket == fc_nbucket)
			{
				IndexTuple	fc_new_itup;

				/*
				 * 复制索引元组，因为我们必须在其上进行修改。
				 */
				fc_new_itup = CopyIndexTuple(fc_itup);

				/*
				 * 将索引元组标记为由拆分移动，这样正在拆分的桶的扫描将
				 * 跳过这些元组。
				 */
				fc_new_itup->t_info |= INDEX_MOVED_BY_SPLIT_MASK;

				/*
				 * 将元组插入新桶。如果它不适合新桶中当前页面，
				 * 我们必须分配一个新的溢出页面，将元组放置在该页面上。
				 */
				fc_itemsz = IndexTupleSize(fc_new_itup);
				fc_itemsz = MAXALIGN(fc_itemsz);

				if (PageGetFreeSpaceForMultipleTuples(fc_npage, fc_nitups + 1) < (fc_all_tups_size + fc_itemsz))
				{
					/*
					 * 在临界区更改共享缓冲区状态，
					 * 否则任何错误可能会使其变得不可恢复。
					 */
					START_CRIT_SECTION();

					_hash_pgaddmultitup(fc_rel, fc_nbuf, fc_itups, fc_itup_offsets, fc_nitups);
					MarkBufferDirty(fc_nbuf);
					/* 在释放锁之前记录拆分操作 */
					fc_log_split_page(fc_rel, fc_nbuf);

					END_CRIT_SECTION();

					/* 放弃锁定，但保持引脚 */
					LockBuffer(fc_nbuf, BUFFER_LOCK_UNLOCK);

					/* 保持整洁 */
					for (fc_i = 0; fc_i < fc_nitups; fc_i++)
						pfree(fc_itups[fc_i]);
					fc_nitups = 0;
					fc_all_tups_size = 0;

					/* 链接到新的溢出页面 */
					fc_nbuf = _hash_addovflpage(fc_rel, fc_metabuf, fc_nbuf, (fc_nbuf == fc_bucket_nbuf));
					fc_npage = BufferGetPage(fc_nbuf);
					fc_nopaque = HashPageGetOpaque(fc_npage);
				}

				fc_itups[fc_nitups++] = fc_new_itup;
				fc_all_tups_size += fc_itemsz;
			}
			else
			{
				/*
				 * 元组保留在此页，因此无需执行任何操作。
				 */
				Assert(fc_bucket == fc_obucket);
			}
		}

		fc_oblkno = fc_oopaque->hasho_nextblkno;

		/* 保留对旧主桶的锁定 */
		if (fc_obuf == fc_bucket_obuf)
			LockBuffer(fc_obuf, BUFFER_LOCK_UNLOCK);
		else
			_hash_relbuf(fc_rel, fc_obuf);

		/* 如果旧桶中没有更多溢出页，则退出循环 */
		if (!BlockNumberIsValid(fc_oblkno))
		{
			/*
			 * 在临界区中更改共享缓冲区状态，否则
			 * 任何错误都可能导致其无法恢复。
			 */
			START_CRIT_SECTION();

			_hash_pgaddmultitup(fc_rel, fc_nbuf, fc_itups, fc_itup_offsets, fc_nitups);
			MarkBufferDirty(fc_nbuf);
			/* 在释放锁之前记录拆分操作 */
			fc_log_split_page(fc_rel, fc_nbuf);

			END_CRIT_SECTION();

			if (fc_nbuf == fc_bucket_nbuf)
				LockBuffer(fc_nbuf, BUFFER_LOCK_UNLOCK);
			else
				_hash_relbuf(fc_rel, fc_nbuf);

			/* 保持整洁 */
			for (fc_i = 0; fc_i < fc_nitups; fc_i++)
				pfree(fc_itups[fc_i]);
			break;
		}

		/* 否则，继续下一个旧页 */
		fc_obuf = _hash_getbuf(fc_rel, fc_oblkno, HASH_READ, LH_OVERFLOW_PAGE);
		fc_opage = BufferGetPage(fc_obuf);
		fc_oopaque = HashPageGetOpaque(fc_opage);
	}

	/*
	 * 我们已到达旧桶链的末尾，因此完成了元组的分区。
	 * 标记旧桶和新桶以指示拆分已完成。
	 *
	 * 为了避免由于桶的锁定顺序导致死锁，首先锁定旧桶
	 * 然后锁定新桶。
	 */
	LockBuffer(fc_bucket_obuf, BUFFER_LOCK_EXCLUSIVE);
	fc_opage = BufferGetPage(fc_bucket_obuf);
	fc_oopaque = HashPageGetOpaque(fc_opage);

	LockBuffer(fc_bucket_nbuf, BUFFER_LOCK_EXCLUSIVE);
	fc_npage = BufferGetPage(fc_bucket_nbuf);
	fc_nopaque = HashPageGetOpaque(fc_npage);

	START_CRIT_SECTION();

	fc_oopaque->hasho_flag &= ~LH_BUCKET_BEING_SPLIT;
	fc_nopaque->hasho_flag &= ~LH_BUCKET_BEING_POPULATED;

	/*
	 * 拆分完成后，标记旧桶以指示它
	 * 包含可删除的元组。我们将在
	 * 拆分结束时、从旧桶进行下一次拆分时或在清理时
	 * 删除这些元组后清除拆分清理标志。
	 */
	fc_oopaque->hasho_flag |= LH_BUCKET_NEEDS_SPLIT_CLEANUP;

	/*
	 * 现在写入缓冲区，此处我们不释放锁，因为调用者
	 * 负责释放锁。
	 */
	MarkBufferDirty(fc_bucket_obuf);
	MarkBufferDirty(fc_bucket_nbuf);

	if (RelationNeedsWAL(fc_rel))
	{
		XLogRecPtr	fc_recptr;
		xl_hash_split_complete fc_xlrec;

		fc_xlrec.old_bucket_flag = fc_oopaque->hasho_flag;
		fc_xlrec.new_bucket_flag = fc_nopaque->hasho_flag;

		XLogBeginInsert();

		XLogRegisterData((char *) &fc_xlrec, SizeOfHashSplitComplete);

		XLogRegisterBuffer(0, fc_bucket_obuf, REGBUF_STANDARD);
		XLogRegisterBuffer(1, fc_bucket_nbuf, REGBUF_STANDARD);

		fc_recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_SPLIT_COMPLETE);

		PageSetLSN(BufferGetPage(fc_bucket_obuf), fc_recptr);
		PageSetLSN(BufferGetPage(fc_bucket_nbuf), fc_recptr);
	}

	END_CRIT_SECTION();

	/*
	 * 如果可能，清理旧桶。我们可能无法做到这一点，
	 * 如果其他人对其有锁定，但如果没有则可以继续。
	 * 这不是绝对必要的，但可以减少膨胀；如果我们现在不做，
	 * VACUUM 最终会做到这一点，但可能要等到分配新的溢出
	 * 页之后。请注意，无需清理新桶。
	 */
	if (IsBufferCleanupOK(fc_bucket_obuf))
	{
		LockBuffer(fc_bucket_nbuf, BUFFER_LOCK_UNLOCK);
		hashbucketcleanup(fc_rel, fc_obucket, fc_bucket_obuf,
						  BufferGetBlockNumber(fc_bucket_obuf), NULL,
						  fc_maxbucket, fc_highmask, fc_lowmask, NULL, NULL, true,
						  NULL, NULL);
	}
	else
	{
		LockBuffer(fc_bucket_nbuf, BUFFER_LOCK_UNLOCK);
		LockBuffer(fc_bucket_obuf, BUFFER_LOCK_UNLOCK);
	}
}

/*
 *	_hash_finish_split() -- 完成先前中断的拆分操作
 *
 * 为了完成拆分操作，我们在新桶中形成 TID 的哈希表，
 * 然后它被拆分操作用于跳过在拆分操作先前中断之前
 * 已移动的元组。
 *
 * 调用者必须持有对元数据页和旧桶的主页缓存的锁定，
 * 但不需要锁定。缓冲区在相同状态下返回。（
 * 只有在必要时才接触元数据页，以添加或删除溢出
 * 页。）
 */
void _hash_finish_split(Relation fc_rel, Buffer fc_metabuf, Buffer fc_obuf, Bucket fc_obucket,
				   uint32 fc_maxbucket, uint32 fc_highmask, uint32 fc_lowmask)
{
	HASHCTL		fc_hash_ctl;
	HTAB	   *fc_tidhtab;
	Buffer		fc_bucket_nbuf = InvalidBuffer;
	Buffer		fc_nbuf;
	Page		fc_npage;
	BlockNumber fc_nblkno;
	BlockNumber fc_bucket_nblkno;
	HashPageOpaque fc_npageopaque;
	Bucket		fc_nbucket;
	bool		fc_found;

	/* 初始化用于跟踪 TIDs 的哈希表 */
	fc_hash_ctl.keysize = sizeof(ItemPointerData);
	fc_hash_ctl.entrysize = sizeof(ItemPointerData);
	fc_hash_ctl.hcxt = CurrentMemoryContext;

	fc_tidhtab =
		hash_create("bucket ctids",
					256,		/* 任意初始大小 */
					&fc_hash_ctl,
					HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);

	fc_bucket_nblkno = fc_nblkno = _hash_get_newblock_from_oldbucket(fc_rel, fc_obucket);

	/*
	 * 扫描新桶并构建 TID 的哈希表
	 */
	for (;;)
	{
		OffsetNumber fc_noffnum;
		OffsetNumber fc_nmaxoffnum;

		fc_nbuf = _hash_getbuf(fc_rel, fc_nblkno, HASH_READ,
							LH_BUCKET_PAGE | LH_OVERFLOW_PAGE);

		/* 记住主桶缓冲区以获取其清理锁。 */
		if (fc_nblkno == fc_bucket_nblkno)
			fc_bucket_nbuf = fc_nbuf;

		fc_npage = BufferGetPage(fc_nbuf);
		fc_npageopaque = HashPageGetOpaque(fc_npage);

		/* 扫描新页中的每个元组 */
		fc_nmaxoffnum = PageGetMaxOffsetNumber(fc_npage);
		for (fc_noffnum = FirstOffsetNumber;
			 fc_noffnum <= fc_nmaxoffnum;
			 fc_noffnum = OffsetNumberNext(fc_noffnum))
		{
			IndexTuple	fc_itup;

			/* 获取项目的 TID 并将其插入哈希表。 */
			fc_itup = (IndexTuple) PageGetItem(fc_npage,
											PageGetItemId(fc_npage, fc_noffnum));

			(void) hash_search(fc_tidhtab, &fc_itup->t_tid, HASH_ENTER, &fc_found);

			Assert(!fc_found);
		}

		fc_nblkno = fc_npageopaque->hasho_nextblkno;

		/*
		 * 释放我们的写锁而不修改缓冲区，并确保
		 * 保留对主桶的锁定。
		 */
		if (fc_nbuf == fc_bucket_nbuf)
			LockBuffer(fc_nbuf, BUFFER_LOCK_UNLOCK);
		else
			_hash_relbuf(fc_rel, fc_nbuf);

		/* 如果新桶中没有更多溢出页，则退出循环 */
		if (!BlockNumberIsValid(fc_nblkno))
			break;
	}

	/*
	 * 有条件地获取旧桶和新桶的清理锁以执行
	 * 拆分操作。如果我们未能获取清理锁，请默默放弃，
	 * 下一次在旧桶上的插入将尝试再次完成拆分。
	 */
	if (!ConditionalLockBufferForCleanup(fc_obuf))
	{
		hash_destroy(fc_tidhtab);
		return;
	}
	if (!ConditionalLockBufferForCleanup(fc_bucket_nbuf))
	{
		LockBuffer(fc_obuf, BUFFER_LOCK_UNLOCK);
		hash_destroy(fc_tidhtab);
		return;
	}

	fc_npage = BufferGetPage(fc_bucket_nbuf);
	fc_npageopaque = HashPageGetOpaque(fc_npage);
	fc_nbucket = fc_npageopaque->hasho_bucket;

	fc__hash_splitbucket(fc_rel, fc_metabuf, fc_obucket,
					  fc_nbucket, fc_obuf, fc_bucket_nbuf, fc_tidhtab,
					  fc_maxbucket, fc_highmask, fc_lowmask);

	_hash_dropbuf(fc_rel, fc_bucket_nbuf);
	hash_destroy(fc_tidhtab);
}

/*
 *	log_split_page() -- 记录分割操作
 *
 *	当新桶中的新页面满时，我们记录分割操作，因此我们记录整个页面。
 *
 *	'buf' 必须由调用者锁定，调用者也负责解锁它。
 */
static void fc_log_split_page(Relation fc_rel, Buffer fc_buf)
{
	if (RelationNeedsWAL(fc_rel))
	{
		XLogRecPtr	fc_recptr;

		XLogBeginInsert();

		XLogRegisterBuffer(0, fc_buf, REGBUF_FORCE_IMAGE | REGBUF_STANDARD);

		fc_recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_SPLIT_PAGE);

		PageSetLSN(BufferGetPage(fc_buf), fc_recptr);
	}
}

/*
 *	_hash_getcachedmetap() -- 返回缓存的元页面数据。
 *
 *	如果 metabuf 不是 InvalidBuffer，调用者必须在元页面上保持一个引脚，但不需要锁定。如果未设置，我们将在返回之前设置它（如果需要刷新缓存），并以引脚返回，但不对其加锁；调用者负责释放引脚。
 *
 *	如果缓存尚未初始化或者 force_refresh 为真，我们将刷新缓存。
 */
HashMetaPage
_hash_getcachedmetap(Relation fc_rel, Buffer *fc_metabuf, bool fc_force_refresh)
{
	Page		fc_page;

	Assert(fc_metabuf);
	if (fc_force_refresh || fc_rel->rd_amcache == NULL)
	{
		char	   *fc_cache = NULL;

		/*
		 * 重要的是我们不要将 rd_amcache 设置为无效值。
		 * MemoryContextAlloc 或 _hash_getbuf 可能会失败，因此在
		 * 两者都成功之前，不要在实际的 relcache 条目中安装指向新分配存储的指针。
		 */
		if (fc_rel->rd_amcache == NULL)
			fc_cache = MemoryContextAlloc(fc_rel->rd_indexcxt,
									   sizeof(HashMetaPageData));

		/* 读取元页面。 */
		if (BufferIsValid(*fc_metabuf))
			LockBuffer(*fc_metabuf, BUFFER_LOCK_SHARE);
		else
			*fc_metabuf = _hash_getbuf(fc_rel, HASH_METAPAGE, HASH_READ,
									LH_META_PAGE);
		fc_page = BufferGetPage(*fc_metabuf);

		/* 填充缓存。 */
		if (fc_rel->rd_amcache == NULL)
			fc_rel->rd_amcache = fc_cache;
		memcpy(fc_rel->rd_amcache, HashPageGetMeta(fc_page),
			   sizeof(HashMetaPageData));

		/* 释放元页面锁，但保留引脚。 */
		LockBuffer(*fc_metabuf, BUFFER_LOCK_UNLOCK);
	}

	return (HashMetaPage) fc_rel->rd_amcache;
}

/*
 *	_hash_getbucketbuf_from_hashkey() -- 获取给定 hashkey 的桶缓冲区。
 *
 *	桶页面在分配后不会移动或被移除。这给了我们一个机会，可以使用之前保存的元页面内容来达到目标桶缓冲区，而不是每次都从元页面中读取。这节省了每次想要到达目标桶缓冲区时的一个缓冲区访问，对 bufmgr 的流量和争用非常有帮助。
 *
 *	访问类型参数（HASH_READ 或 HASH_WRITE）指示桶缓冲区是否需要锁定以进行读取或写入。
 *
 *	输出参数 cachedmetap 设置为用于 hashkey 到桶缓冲区映射的元页面内容。一些调用者需要这个信息以在桶分割的情况下到达旧桶，请参见 _hash_doinsert()。
 */
Buffer _hash_getbucketbuf_from_hashkey(Relation fc_rel, uint32 fc_hashkey, int fc_access,
								HashMetaPage *fc_cachedmetap)
{
	HashMetaPage fc_metap;
	Buffer		fc_buf;
	Buffer		fc_metabuf = InvalidBuffer;
	Page		fc_page;
	Bucket		fc_bucket;
	BlockNumber fc_blkno;
	HashPageOpaque fc_opaque;

	/* 我们从目标桶缓冲区读取，因此必须加锁。 */
	Assert(fc_access == HASH_READ || fc_access == HASH_WRITE);

	fc_metap = _hash_getcachedmetap(fc_rel, &fc_metabuf, false);
	Assert(fc_metap != NULL);

	/*
	 * 循环直到我们在正确的目标桶上获取锁。
	 */
	for (;;)
	{
		/*
		 * 计算目标桶编号，并转换为块编号。
		 */
		fc_bucket = _hash_hashkey2bucket(fc_hashkey,
									  fc_metap->hashm_maxbucket,
									  fc_metap->hashm_highmask,
									  fc_metap->hashm_lowmask);

		fc_blkno = BUCKET_TO_BLKNO(fc_metap, fc_bucket);

		/* 获取该桶的主桶页面 */
		fc_buf = _hash_getbuf(fc_rel, fc_blkno, fc_access, LH_BUCKET_PAGE);
		fc_page = BufferGetPage(fc_buf);
		fc_opaque = HashPageGetOpaque(fc_page);
		Assert(fc_opaque->hasho_bucket == fc_bucket);
		Assert(fc_opaque->hasho_prevblkno != InvalidBlockNumber);

		/*
		 * 如果这个桶没有被分割，我们就完成了。
		 */
		if (fc_opaque->hasho_prevblkno <= fc_metap->hashm_maxbucket)
			break;

		/* 释放此缓冲区上的锁，更新缓存的元页面，并重试。 */
		_hash_relbuf(fc_rel, fc_buf);
		fc_metap = _hash_getcachedmetap(fc_rel, &fc_metabuf, true);
		Assert(fc_metap != NULL);
	}

	if (BufferIsValid(fc_metabuf))
		_hash_dropbuf(fc_rel, fc_metabuf);

	if (fc_cachedmetap)
		*fc_cachedmetap = fc_metap;

	return fc_buf;
}
